iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Software Development

Datomic,內建事件溯源的資料庫。系列 第 13

先從 Datalog 談起 -- part 8 (predicates and transformation functions)

  • 分享至 

  • xImage
  •  

首先,我們要先定義兩個詞彙:斷言與轉換函數。

  • 斷言 (predicate) 是一種特殊函數,它會接受判斷條件做為輸入,而它的輸出只有真 (true) 或是假 (false) 兩種可能性。以下方的的例子來講,even? 就是一個斷言。
(even? 0) 
;; => true
(even? 1)
;; => false
(filter even? (range 10))
;;=> (0 2 4 6 8)
  • 轉換函數 (transformation function) 是純函數 (pure function),它會接受資料做為輸入,而它的輸出是將輸入的資料做某些轉換。在下方的例子,add-2 就是一個轉換函數。我們也可以說,斷言是一種轉換函數的特例。
(defn add-2  [x]
  (+ 2 x))

(add-2 x)
;; => 4

(map add-2 [0 1 2 3 4 5])
;; => (2 3 4 5 6 7)

五種 where 子句

根據 Datalog 的文件,:where 開頭之後的子句有五種:(註1)

  1. not 子句
  2. not-join 子句
  3. or 子句
  4. or-join 子句
  5. 表達式子句 (expression clause)

其中 not, not-join, or, or-join 子句已經在上一篇提到過了。表達式子句則有四種型態:

  1. 資料模式 (data pattern)
    • 資料模式是最常用的子句。
  2. 斷言表達式 (predicate expression)
  3. 函數表達式 (function expression)
    • 此處的函數是轉換函數。
  4. 規則表達式 (rule expression)

斷言表達式

考慮如下的 SQL 查詢,它是查詢『 1984 年之前的電影』。

SELECT title
FROM movie
WHERE year < 1984;

如果用 Datalog 來改寫的話:

[:find ?title
 :where
 [?m :movie/title ?title]
 [?m :movie/year ?year]
 [(< ?year 1984)]]

在上頭的 Datalog 查詢裡, [(< ?year 1984)]] 是一個斷言表達式,它的傳回值只有「真」或是「假」兩種可能。當傳回真的時候,對應到的變數會被加入查詢結果裡;當傳回假的時候,對應到的變數則會從查詢結果裡加以移除。

這邊有兩點值得特別注意:

  1. 一些常用的斷言,比方說: <, >, <=, >=, =, not= 都已內建在 Datalog 語言裡,開箱即可用。
  2. 當我們搭配 Clojure/Java/Kotlin 之類的語言來使用 Datomic 資料庫時,我們可以將自訂斷言 (user-defined predicate) 傳入 Datalog 查詢裡做為斷言。

函數表達式

Datomic 資料庫可以儲存時間,而時間適合使用 :db.type/instant 資料型態來儲存。(註2) 資料庫儲存的時間型態,我們可以看成是 java.util.Date,於是如果要對這個時間做加、減法時,我們需要先將其轉換為 java.lang.Long 型態的時間戳 (timestamp)。

下方是一個 Clojure 的函數,它可以根據生日時間 (birthday) 與現在時間 (today) 來算出年齡 (age)。

(defn age [birthday today]
  (quot (- (.getTime today)
           (.getTime birthday))
        (* 1000 60 60 24 365)))

這邊解釋它的實作:

  1. 接受兩個時間 (資料型態是 java.util.Date) 分別是「出生時間」與「今日時間」,先用 .getTime 轉換這兩個時間為長整數資料型態的時間戳。
  2. 將時間戳相減,算出差距。
  3. 將相差的時間戳 (單位為毫秒),除以一年的總毫秒數,於是可以算出年齡並且傳回。(quot 是除法。)

一旦有了 age 函數,我們就可以透過 Datalog 查詢來算出人的年齡,下方的 Datalog 查詢是『給定人名與現在時間,用人名去查出這個人的出生時間,然後用 tutorial.fns/age 這個函數去算出這個人現在的年齡,並且傳回。』

[:find ?age
 :in $ ?name ?today
 :where
 [?p :person/name ?name]
 [?p :person/born ?born]
 [(tutorial.fns/age ?born ?today) ?age]]

在上頭的 Datalog 查詢裡,[(tutorial.fns/age ?born ?today) ?age]] 是一個函數表達式,它的形式是 [(<fn> <arg1> <arg2> ...) <result-binding>] 。它會把函數運算 (<fn> <arg1> <arg2> ...) 的結果,綁定到 <result-binding> 這個變數裡。

這邊有三點值得特別注意:

  1. 所有定義在 clojure.core 這個命名空間裡的純函數,都可以拿來當函數使用,除了 eval 之外。
  2. 函數表達式裡用來綁定函數運算結果的變數,它可以是四種不同的形式:scalar, tuple, colllection, relation。這四種形式與 Datalog 查詢的變數綁定容許的四種形式一模一樣。
  3. 當我們搭配 Clojure/Java/Kotlin 之類的語言來使用 Datomic 資料庫時,我們可以將自訂函數 (user-defined function) 傳入 Datalog 查詢裡做為轉換函數。(在這個例子裡,我們就是使用自訂函數,讀者可以注意到,當我們呼叫自訂函數時,必須加上完整的命名空間,即 tutorial.fns )

執行環境 (runtime)

有 SQL 使用經驗的讀者,可能會想到執行環境 (runtime) ,因而覺得好像 Datalog 的「自訂斷言」與「自訂函數」有點太靈活了,不像是真的。在 SQL 資料庫,如果我們要使用自訂函數 (user-defined function)、儲存程序 (stored procedure) 的話,我們都必須專程把這些東西,安裝到資料庫裡,否則資料庫的執行環境無法調用它們。這是因為 SQL 資料庫與後端程式 (backend program) 通常是兩個獨立的執行環境。

然而,在上述的範例裡,卻沒有『安裝自訂函數』的步驟,彷彿後端程式與 Datomic 資料庫是在同一個執行環境裡一樣?

這個現象跟 Datomic 獨樹一格的軟體架構有關。在 Datomic 資料庫,因為讀寫完全分離,所以讀的部分即資料庫的查詢,它運作的執行環境是在後端程式裡。這就是為什麼我們使用自訂函數、自訂斷言,卻不需要做對應的安裝的原因。

練習題

註:

  1. 五種 where 子句
  2. Datomic 支援的資料型態

其它資源

  1. 歡迎訂閱 PruningSuccess 電子報,主要談論軟體開發、資料處理、資料分析等議題。
  2. 歡迎加入 Clojure 社群

上一篇
先從 Datalog 談起 -- part 7 (more join)
下一篇
先從 Datalog 談起 -- part 9 (aggregates and with clause)
系列文
Datomic,內建事件溯源的資料庫。25
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言